package com.db.junit;

import com.db.junit.exception.*;
import com.db.junit.state.FirstFailedAttempt;
import com.db.junit.state.LoginServiceState;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.ConcurrentHashMap;

/**
 * There are several rules for logging in to our system:
 * When logging in, a user provides a user name and password
 * The user name is a non-blank, unique string containing just about any characters.
 * The password is a non-blank, unique string containing just about any characters.
 * If a user attempts to log in and provides a valid account but invalid password three times, their account is revoked
 * However, if, for the same session, the user uses different account names, then the account will not be revoked (e.g.
 *   try to log in as yury1, yury2, yury1, three failed attempts but the user name is changed, no account gets revoked)
 * Not existing user can not login
 * A user cannot login to a revoked account
 * If failing attempts are not consecutive then no Revoke on Account should be invoked
 * A user must change their password every 30 days, so if they attempt to log in and the password is expired, then
 *   the user must change their password before they can successfully log in
 * A user cannot use any of their previous 24 passwords
 * User accounts have a time stamp of the last time the account was used (logged in, logged out, password changed). If
 *   an account has not been used for 45 days, then the account becomes revoked and can only be enabled by calling
 *   customer support.
 */

public class LoginService {
    private final IAccountRepository accountRepository;
    private final ConcurrentHashMap<String, AccountService> accountServiceMap = new ConcurrentHashMap<>();
    private final ITimeUtil timeUtil;

    public static final long PASSWORD_EXPIRED_TIME = 30 * 24 * 60 * 60;
    public static final long ACCOUNT_EXPIRED_TIME = 45 * 24 * 60 * 60;
    public static final int OLD_PASSWORD_COUNT = 24;

    public LoginService(IAccountRepository accountRepository, ITimeUtil timeUtil) {
        this.accountRepository = accountRepository;
        this.timeUtil = timeUtil;
    }

    public void login(String userName, String password) throws AccountNotFoundException, AccountRevokedException, PasswordExpiredException {
        IAccount account = getAccount(userName);
        checkPasswordExpiredTime(account);

        getAccountService(userName).getState().login(account, password, this);
        account.updateLastActivityTime();
    }

    public void logout(String userName) throws AccountNotFoundException, AccountRevokedException {
        IAccount account = getAccount(userName);

        account.setLoggedIn(false);
        account.updateLastActivityTime();
    }

    public void changePassword(String userName, String password) throws AccountNotFoundException, AccountRevokedException, PasswordRepeatedException {
        IAccount account = getAccount(userName);
        AccountService accountService = getAccountService(userName);
        if (accountService.checkOldPassword(password)) {
            throw new PasswordRepeatedException("Password is repeated");
        }

        account.setPassword(password);
        account.updatePasswordChangeTime();
        accountService.addOldPassword(password);
        account.updateLastActivityTime();
    }

    public void setState(String userName, LoginServiceState state) {
        accountServiceMap.get(userName).setState(state);
    }

    private IAccount getAccount(String userName) throws AccountRevokedException, AccountNotFoundException {
        IAccount account = accountRepository.find(userName);
        if (account != null) {
            checkAccountExpiredTime(account);
            return account;
        } else {
            throw new AccountNotFoundException("Account is not found");
        }
    }

    private AccountService getAccountService(String userName) {
        accountServiceMap.putIfAbsent(userName, new AccountService());
        return accountServiceMap.get(userName);
    }

    private void checkPasswordExpiredTime(IAccount account) throws PasswordExpiredException {
        if ((timeUtil.currentTimeStamp() - account.getPasswordChangeTime()) >= PASSWORD_EXPIRED_TIME) {
            throw new PasswordExpiredException("Password is expired");
        }
    }

    private void checkAccountExpiredTime(IAccount account) throws AccountRevokedException {
        if ((timeUtil.currentTimeStamp() - account.getLastActivityTime()) >= ACCOUNT_EXPIRED_TIME) {
            account.setRevoked(true);
            throw new AccountRevokedException("Account is revoked");
        }
    }

    private class AccountService {
        private LoginServiceState state = new FirstFailedAttempt();
        private final String[] oldPasswords = new String[OLD_PASSWORD_COUNT];
        private int oldPasswordIndex;

        public LoginServiceState getState() {
            return state;
        }

        public void setState(LoginServiceState state) {
            this.state = state;
        }

        public boolean checkOldPassword(String password) {
            for (String pass : oldPasswords) {
                if (password.equals(pass)) {
                    return true;
                }
            }
            return false;
        }

        public void addOldPassword(String password) {
            oldPasswords[oldPasswordIndex++ % OLD_PASSWORD_COUNT] = password;
        }
    }
}
